// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ResumableFileDownloader.cs" company="XamlNinja">
//   2011 Richard Griffin and Ollie Riches
// </copyright>
// <summary>
//   ResumableFileDownloader.cs
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace WP7Contrib.Services.DownloadManager
{
    using System;
    using System.Linq;
    using System.Net;
    using System.Net.Browser;
    using System.Reactive.Linq;
    using System.Reflection;
    using System.Threading;
    using System.Windows;
    using System.Windows.Threading;
    using Collections;
    using Common;
    using Common.Extensions;
    using Communications;
    using Logging;
    using Serialization.Helpers;
    using Storage;

    /// <summary>
    /// Resumable file downloader, will attempt to download files using the HTTP header value 'accept-range'. Every block download will be persisted to local 
    /// storage allowing the downloader to be stop & restarted at will. The requested file downloads will be persisted to isolated storage so that requests are 
    /// persisted across application tombstoning and restarts.
    /// </summary>
    public sealed class ResumableFileDownloader : BaseModel,  IDownloadService, IDisposable
    {
        private const string FailedStart = "ResumableFileDownloader: Failed to start downloader, message - '{0}'";

        private const string FailedStop = "ResumableFileDownloader: Failed to stop downloader, message - '{0}'";

        private const string FailedAdd = "ResumableFileDownloader: Failed to add request to downloader, message - '{0}'";

        private const string FailedRemove = "ResumableFileDownloader: Failed to remove request from downloader, message - '{0}'";

        private const string FailedPrioritise = "ResumableFileDownloader: Failed to prioritise request to downloader, message - '{0}'";

        private const string FailedRemoveAll = "ResumableFileDownloader: Failed to remove all requests from downloader, message - '{0}'";

        private const string FailedRemoveCompleted = "ResumableFileDownloader: Failed to remove completed requests from downloader, message - '{0}'";

        private const string FailedRemoveFailed = "ResumableFileDownloader: Failed to remove failed requests from downloader, message - '{0}'";

        public event EventHandler<DownloaderStateEventArgs> DownloadStateChanged = delegate { };

        public event EventHandler<FileRequestEventArgs> DownloadStarted = delegate { };
        public event EventHandler<FileRequestEventArgs> DownloadUpdated = delegate { };
        public event EventHandler<FileRequestEventArgs> DownloadStopped = delegate {};
        public event EventHandler<FileRequestEventArgs> DownloadFailed = delegate {};
        public event EventHandler<FileRequestEventArgs> DownloadCompleted = delegate {};

        private readonly ObservableCollection<PersistedFileRequest> persistedRequests;
        private readonly ObservableCollection<FileRequest> requests;

        private readonly ISettings settings;
        private IStorage storage;
        private readonly INetworkService networkService;
        private ILog log;

        private NetworkType networkType;
        private IDisposable networkTypeObserver;

        private volatile DownloaderState state;
        private Thread processingThread;
        private readonly AutoResetEvent pending;
        private readonly AutoResetEvent networkTypePending;
        private readonly object sync;
        private readonly object persistSync;

        private readonly Dispatcher dispatcher;

        public ResumableFileDownloader()
            : this(new Settings(), new StorageService(new[] { typeof(SerializeUri).Assembly }), new NetworkMonitor(500), new DebugLog())
        {
        }

        public ResumableFileDownloader(ISettings settings, IStorageService storageService, INetworkService networkService)
            : this(settings, storageService, networkService, new DebugLog())
        {
        }
        
        public ResumableFileDownloader(ISettings settings, IStorageService storageService, INetworkService networkService, ILog log)
        {
            this.dispatcher = Deployment.Current.Dispatcher;

            this.settings = settings;
            this.storage = storageService.OpenPersistent("ResumableFileDownloader");
            this.networkService = networkService;
            this.log = log;

            this.State = DownloaderState.Initialising;
            
            WebRequest.RegisterPrefix("http://", WebRequestCreator.ClientHttp);
            WebRequest.RegisterPrefix("https://", WebRequestCreator.ClientHttp);

            this.requests = new ObservableCollection<FileRequest>();
            this.persistedRequests = this.storage.Read<ObservableCollection<PersistedFileRequest>>("requests") ?? new ObservableCollection<PersistedFileRequest>();
            this.persistedRequests.ForEach(pr =>
                                               {
                                                   pr.FileLength = FileRequestHelper.GetFileLength(pr);
                                                   this.requests.Add(new FileRequest(pr));
                                               });

            this.pending = new AutoResetEvent(false);
            this.networkTypePending = new AutoResetEvent(false);
            this.sync = new object();
            this.persistSync = new object();

            this.networkTypeObserver = this.networkService.Status()
                .Subscribe(this.HandleNetworkTypeChanged);

            this.State = DownloaderState.Ready;
        }
        
        public void Dispose()
        {
            if (this.storage != null)
            {
                this.PersistRequests();

                this.storage.Dispose();
                this.storage = null;
            }

            if (this.networkTypeObserver != null)
            {
                this.networkTypeObserver.Dispose();
                this.networkTypeObserver = null;
            }

            if (this.State == DownloaderState.Processing || this.State == DownloaderState.PausedWaiting || this.State == DownloaderState.PausedNetwork)
            {
                this.Stop();
                this.processingThread.Join();
            }

            this.processingThread = null;
            this.persistedRequests.Clear();
            this.requests.Clear();
        }
        
        public int BlockSize
        {
            get { return this.settings.BlockSize; }
        }

        public ReadOnlyObservableCollection<FileRequest> Requests
        {
            get { return new ReadOnlyObservableCollection<FileRequest>(this.requests); }
        }

        public ReadOnlyObservableCollection<NetworkType> NetworkTypes
        {
            get { return new ReadOnlyObservableCollection<NetworkType>(this.settings.NetworkTypes); }
        }

        public NetworkType NetworkType
        {
            get
            {
                return this.networkType;
            }
            private set
            {
                if (this.networkType == value)
                {
                    return;
                }

                this.networkType = value;
                this.log.Write("ResumableFileDownloader: Network type - '{0}'", this.networkType);
                this.dispatcher.BeginInvoke(() => this.RaisePropertyChanged(() => NetworkType));
            }
        }

        public DownloaderState State
        {
            get
            {
                return this.state;
            }
            private set
            {
                if (this.state == value)
                {
                    return;
                }

                this.state = value;
                this.log.Write("ResumableFileDownloader: State - '{0}'", this.state);
                this.dispatcher.BeginInvoke(() =>
                                             {
                                                 this.RaisePropertyChanged(() => State);
                                                 this.DownloadStateChanged(this, new DownloaderStateEventArgs(this.State));
                                             });
            }
        }

        public ISettings Settings
        {
            get { return this.settings; }
        }

        public IDownloadService Start()
        {
            this.log.Write("ResumableFileDownloader: Start...");

            try
            {
                lock (this.sync)
                {
                    if (this.State != DownloaderState.Ready)
                    {
                        return this;
                    }

                    this.State = DownloaderState.Starting;
                }
                this.processingThread = new Thread(this.ProcessRequests);
                this.processingThread.Start();

                return this;
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedStart, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        public IDownloadService Stop()
        {
            this.log.Write("ResumableFileDownloader: Stop...");

            try
            {
                lock (this.sync)
                {
                    if (this.State != DownloaderState.Processing && this.State != DownloaderState.PausedWaiting && this.State != DownloaderState.PausedNetwork)
                    {
                        return this;
                    }

                    var currentState = this.State;
                    this.State = DownloaderState.Stopping;

                    if (currentState == DownloaderState.PausedWaiting)
                    {
                        this.pending.Set();
                    }
                    else if (currentState == DownloaderState.PausedNetwork)
                    {
                        this.networkTypePending.Set();
                    }
                }

                ThreadPool.QueueUserWorkItem(s =>
                {
                    this.processingThread.Join();
                    this.State = DownloaderState.Ready;
                });

                return this;
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedStop, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        public IDownloadService Add(string url, string filePath)
        {
            FileRequest request = null;
            return this.Add(url, filePath, ref request);
        }

        public IDownloadService Add(string url, string filePath, ref FileRequest request)
        {
            this.log.Write("ResumableFileDownloader: Add...");
            this.log.Write("ResumableFileDownloader: Url - '{0}'", url);
            this.log.Write("ResumableFileDownloader: File path - '{0}'", filePath);

            try
            {
                this.Validate(url, filePath);
            
                var newRequest = new PersistedFileRequest { UrlAsString = url, FilePath = filePath };
            
                lock(sync)
                {
                    this.persistedRequests.Add(newRequest);

                    request = new FileRequest(newRequest);
                    this.requests.Add(request);

                    this.PersistRequests();

                    if (this.State == DownloaderState.PausedWaiting)
                    {
                        this.pending.Set();
                    }
                }

                return this;
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedAdd, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        public IDownloadService PriorityAdd(string url, string filePath)
        {
            FileRequest request = null;
            return this.PriorityAdd(url, filePath, ref request);
        }

        public IDownloadService PriorityAdd(string url, string filePath, ref FileRequest request)
        {
            this.log.Write("ResumableFileDownloader: Prioritise...");
            this.log.Write("ResumableFileDownloader: Url - '{0}'", url);
            this.log.Write("ResumableFileDownloader: File path - '{0}'", filePath);

            try
            {
                this.Validate(url, filePath);

                var newRequest = new PersistedFileRequest { UrlAsString = url, FilePath = filePath };
                
                var restartRequired = false;
                if (this.State == DownloaderState.Processing || this.State == DownloaderState.PausedWaiting || this.State == DownloaderState.PausedNetwork)
                {
                    restartRequired = true;
                    this.Stop();
                    this.State = DownloaderState.Ready;
                }

                lock (this.sync)
                {
                    this.persistedRequests.Insert(0, newRequest);
                    request = new FileRequest(newRequest);

                    var localRequest = request;
                    this.dispatcher.BeginInvoke(() => this.requests.Insert(0, localRequest));
                    
                    this.PersistRequests();
                }

                if (restartRequired)
                {
                    this.Start();
                }

                return this;
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedPrioritise, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        public IDownloadService Prioritise(FileRequest request)
        {
            this.log.Write("ResumableFileDownloader: Prioritise...");
            this.log.Write("ResumableFileDownloader: Identifier - '{0}'", request.Identifier);
            this.log.Write("ResumableFileDownloader: Url - '{0}'", request.Url);

            try
            {
                var exists = this.persistedRequests.Count(pr => pr.Identifier == request.Identifier) != 0;
                if (!exists)
                {
                    return this;
                }

                var restartRequired = false;
                if (this.State == DownloaderState.Processing || this.State == DownloaderState.PausedWaiting || this.State == DownloaderState.PausedNetwork)
                {
                    restartRequired = true;
                    this.Stop();
                    this.State = DownloaderState.Ready;
                }

                lock (this.sync)
                {
                    var persistedRequest = this.persistedRequests.SingleOrDefault(pr => pr.Identifier == request.Identifier);
                    if (persistedRequest == null)
                    {
                        return this;
                    }

                    this.persistedRequests.Remove(persistedRequest);
                    this.persistedRequests.Insert(0, persistedRequest);

                    var localRequest = request;
                    this.dispatcher.BeginInvoke(() =>
                                                    {
                                                        this.requests.Remove(localRequest);
                                                        this.requests.Insert(0, localRequest);
                                                    });

                    this.PersistRequests();
                }

                if (restartRequired)
                {
                    this.Start();
                }

                return this;
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedPrioritise, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        public IDownloadService Remove(FileRequest request)
        {
            this.log.Write("ResumableFileDownloader: Remove...");
            
            try
            {
                if (request == null)
                {
                    return this;
                }

                this.log.Write("ResumableFileDownloader: Url - '{0}'", request.Url);
                this.log.Write("ResumableFileDownloader: File path - '{0}'", request.FilePath);

                if (string.IsNullOrEmpty(request.Url))
                {
                    return this;
                }

                if (string.IsNullOrEmpty(request.FilePath))
                {
                    return this;
                }

                if (this.persistedRequests.Where(r => r.Identifier == request.Identifier).Count() == 0)
                {
                    return this;
                }

                ThreadPool.QueueUserWorkItem(s =>
                {
                    var restartRequired = false;
                    if (this.State == DownloaderState.Processing || this.State == DownloaderState.PausedWaiting || this.State == DownloaderState.PausedNetwork)
                    {
                        restartRequired = true;
                        this.Stop();

                        this.processingThread.Join();
                    }

                    lock (this.sync)
                    {
                        var persistedItems = this.persistedRequests.Where(r => r.Identifier == request.Identifier);
                        persistedItems.ForEach(r => this.persistedRequests.Remove(r));

                        this.dispatcher.BeginInvoke(() =>
                        {
                            var items = this.requests.Where(r => r.Identifier == request.Identifier);
                            items.ForEach(r => this.requests.Remove(r));
                        });
                        
                        this.PersistRequests();
                    }

                    if (restartRequired)
                    {
                        this.Start();
                    }
                });

                return this;
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedRemove, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        public IDownloadService RemoveAll()
        {
            this.log.Write("ResumableFileDownloader: RemoveAll...");

            try
            {
                if (this.persistedRequests.Count == 0)
                {
                    return this;
                }

                ThreadPool.QueueUserWorkItem(s =>
                {
                    var restartRequired = false;
                    if (this.State == DownloaderState.Processing || this.State == DownloaderState.PausedWaiting || this.State == DownloaderState.PausedNetwork)
                    {
                        restartRequired = true;
                        this.Stop();

                        this.processingThread.Join();
                    }

                    lock (this.sync)
                    {
                        this.persistedRequests.Clear();
                        this.dispatcher.BeginInvoke(() => this.requests.Clear());

                        this.PersistRequests();
                    }

                    if (restartRequired)
                    {
                        this.Start();
                    }
                });

                return this;
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedRemoveAll, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        public IDownloadService RemoveFailed()
        {
            this.log.Write("ResumableFileDownloader: RemoveFailed...");

            try
            {
                lock (this.sync)
                {
                    var failedPersistedRequests = this.persistedRequests.Where(r => r.State == FileRequestState.Failed).ToList();
                    failedPersistedRequests.ForEach(r => this.persistedRequests.Remove(r));

                    this.dispatcher.BeginInvoke(() =>
                    {
                        var failedRequests = this.requests.Where(r => r.State == FileRequestState.Failed).ToList();
                        failedRequests.ForEach(r => this.requests.Remove(r));
                    });
                    
                    this.PersistRequests();
                }

                return this;
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedRemoveFailed, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        public IDownloadService RemoveCompleted()
        {
            this.log.Write("ResumableFileDownloader: RemoveCompleted...");

            try
            {
                lock (this.sync)
                {
                    var completedPersistedRequests = this.persistedRequests.Where(r => r.State == FileRequestState.Completed).ToList();
                    completedPersistedRequests.ForEach(cr => this.persistedRequests.Remove(cr));

                    this.dispatcher.BeginInvoke(() =>
                    {
                        var completedRequests = this.requests.Where(r => r.State == FileRequestState.Completed).ToList();
                        completedRequests.ForEach(r => this.requests.Remove(r));
                    });
                
                    this.PersistRequests();
                }

                return this;
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedRemoveCompleted, exn.Message);
                this.log.Write(message);
                throw new ServiceException(message, exn);
            }
        }

        private void HandleNetworkTypeChanged(NetworkType type)
        {
            this.NetworkType = type;

            if (this.settings.NetworkTypes.Contains(this.NetworkType))
            {
                if (this.State == DownloaderState.PausedNetwork)
                {
                    this.networkTypePending.Set();
                }
            }
        }

        private void PersistRequests()
        {
            lock (persistSync)
            {
                this.storage.Write("requests", this.persistedRequests);
                this.storage.Flush();
            }
        }

        private void DeleteFile(PersistedFileRequest request)
        {
            this.log.Write("ResumableFileDownloader: DeleteFile...");
            
            try
            {
                this.log.Write("ResumableFileDownloader: Url - '{0}'", request.UrlAsString);
                this.log.Write("ResumableFileDownloader: File path - '{0}'", request.FilePath);

                FileRequestHelper.DeleteFile(request);
            }
            catch (Exception exn)
            {
                this.log.Write("ResumableFileDownloader: Failed to delete request files, message - '{0}'", exn.Message);
            }
        }

        private void Validate(string url, string filePath)
        {
            this.log.Write("ResumableFileDownloader: Validate...");

            if (string.IsNullOrEmpty(url))
            {
                throw new ArgumentNullException("url", "Download URL is null or empty!");
            }

            if (string.IsNullOrEmpty(filePath))
            {
                throw new ArgumentNullException("filePath", "Download file path is null or empty!");
            }
        }

        private void ProcessRequests()
        {
            this.log.Write("ResumableFileDownloader: Processing thread started...");

            while(true)
            {
                if (this.State == DownloaderState.Stopping)
                {
                    break;
                }

                if (!this.NetworkTypes.Contains(this.NetworkType))
                {
                    this.State = DownloaderState.PausedNetwork;
                    this.networkTypePending.WaitOne();
                }

                PersistedFileRequest currentRequest;
                
                lock(sync)
                {
                    currentRequest = this.persistedRequests.FirstOrDefault(r => r.State != FileRequestState.Completed && r.State != FileRequestState.Failed);
                }

                if (this.State == DownloaderState.Stopping)
                {
                    break;
                }

                this.State = DownloaderState.Processing;

                if (currentRequest != null)
                {
                    this.log.Write("ResumableFileDownloader: Processing - {0}", currentRequest.UrlAsString);
                    
                    if (currentRequest.State != FileRequestState.Started)
                    {
                        this.log.Write("ResumableFileDownloader: Deleting any existing temporary download file...");
                        FileRequestHelper.DeleteFile(currentRequest);

                        currentRequest.State = FileRequestState.Started;
                    }
                    
                    this.PersistRequests();

                    this.AsynchronousFileDownload(currentRequest);
                }
                else
                {
                    this.State = DownloaderState.PausedWaiting;
                }

                this.pending.WaitOne();
            }

            this.PersistRequests();
            this.log.Write("ResumableFileDownloader: Processing thread stopped...");
        }

        private void AsynchronousFileDownload(PersistedFileRequest currentRequest)
        {
            ThreadPool.QueueUserWorkItem(w =>
            {
                var resumable = this.IsResumableDownload(currentRequest);

                if (currentRequest.State == FileRequestState.Failed)
                {
                    this.log.Write("ResumableFileDownloader: Failed to determine if download is resumable...");
                }
                else
                {
                    Exception exception = null;
                    var retryCount = 0;
                    var completed = false;

                    while (!completed && retryCount != this.Settings.RetryCount)
                    {
                        this.log.Write("ResumableFileDownloader: Retry - {0} of {1}", (retryCount + 1), this.settings.RetryCount);
                        exception = null;

                        try
                        {
                            if (resumable)
                            {
                                this.ResumableDownload(currentRequest);
                            }
                            else
                            {
                                this.NonResumableDownload(currentRequest);
                            }

                            completed = true;
                        }
                        catch (Exception exn)
                        {
                            completed = false;
                            exception = exn;
                        }
                        finally
                        {
                            this.PersistRequests();
                        }

                        retryCount++;
                    }

                    if (exception != null)
                    {
                        this.HandleProcessingException(currentRequest, exception);
                    }

                    lock (sync)
                    {
                        this.pending.Set();
                    }
                }
            });
        }

        private bool IsResumableDownload(PersistedFileRequest currentRequest)
        {
            var localSync = new AutoResetEvent(false);

            var request = (HttpWebRequest)WebRequestCreator.ClientHttp.Create(currentRequest.Url);
            request.Method = "HEAD";

            Exception failed = null;
            var startTime = DateTime.Now;
            Observable.FromAsyncPattern(request.BeginGetResponse, ar =>
            {
                try
                {
                    var response = (HttpWebResponse)request.EndGetResponse(ar);
                    foreach (var key in response.Headers.AllKeys)
                    {
                        this.log.Write("ResumableFileDownloader: Response header, name - '{0}', value - '{1}'", key, response.Headers[key]);
                    }

                    var acceptsRange = response.Headers["Accept-Ranges"];

                    if (acceptsRange != null && acceptsRange.Equals("bytes", StringComparison.CurrentCultureIgnoreCase))
                    {
                        var lengthAsString = response.Headers["Content-Length"];
                        currentRequest.ContentLength = long.Parse(lengthAsString);
                        currentRequest.Resumable = true;
                    }
                    else
                    {
                        currentRequest.Resumable = false;
                    }

                    var finishTime = DateTime.Now;
                    currentRequest.Duration += finishTime - startTime;

                    localSync.Set();
                }
                catch (Exception exn)
                {
                    failed = exn;
                }
            })();

            localSync.WaitOne();

            if (failed != null)
            {
                this.HandleProcessingException(currentRequest, failed);
            }
            
            return currentRequest.Resumable;
        }

        private void ResumableDownload(PersistedFileRequest currentRequest)
        {
            var fileRequest = this.requests.Where(r => r.Identifier == currentRequest.Identifier).Single();
            var localSync = new AutoResetEvent(false);

            var exit = false;
            var completed = false;
            Exception failed = null;

            while(!exit)
            {
                if (currentRequest.FileLength == currentRequest.ContentLength && currentRequest.ContentLength != 0)
                {
                    completed = true;
                    break;
                }

                if (!this.NetworkTypes.Contains(this.NetworkType))
                {
                    break;
                }
                
                if (this.State == DownloaderState.Stopping)
                {
                    break;
                }

                var start = currentRequest.FileLength;
                var end = currentRequest.FileLength + (this.settings.BlockSize - 1);

                if (end > (currentRequest.ContentLength - 1))
                {
                    end = currentRequest.ContentLength - 1;
                }

                var range = string.Format("bytes={0}-{1}", start, end);
                this.log.Write("ResumableFileDownloader: Range - {0}", range);

                var startTime = DateTime.Now;
                var webClient = new WebClient();
                webClient.Headers[HttpRequestHeader.Range] = range;
                
                webClient.OpenReadCompleted += (sender, args) =>
                {
                    if (!this.NetworkTypes.Contains(this.NetworkType))
                    {
                        exit = true;
                    }
                    else
                    {
                        if (args.Error != null)
                        {
                            failed = args.Error;
                            exit = true;
                        }
                        else
                        {
                            try
                            {
                                range = webClient.ResponseHeaders[HttpRequestHeader.ContentRange];
                                var tokens = range.Split('/');
                                currentRequest.ContentLength = Convert.ToInt32(tokens[1]);

                                using (var stream = args.Result)
                                {
                                    var data = new byte[stream.Length];
                                    stream.Read(data, 0, (int) stream.Length);

                                    currentRequest.FileLength += stream.Length;

                                    var finishTime = DateTime.Now;
                                    currentRequest.Duration += finishTime - startTime;

                                    FileRequestHelper.WriteToFile(currentRequest, data);
                                    this.DownloadUpdated(this, new FileRequestEventArgs(fileRequest));
                                }
                            }
                            catch (Exception exn)
                            {
                                exit = true;
                                if (this.NetworkTypes.Contains(this.NetworkType))
                                {
                                    failed = exn;
                                }
                            }
                        }
                    }

                    localSync.Set();
                };

                try
                {
                    webClient.OpenReadAsync(currentRequest.Url);
                    localSync.WaitOne();
                }
                catch (Exception exn)
                {
                    failed = exn;
                }
            }

            if (failed != null)
            {
                throw failed;
            }

            if (completed)
            {
                this.HandleProcessingCompleted(currentRequest);
            }

            if (this.State == DownloaderState.Stopping)
            {
                this.HandleProcessingStopping(currentRequest);
            }
        }

        private void NonResumableDownload(PersistedFileRequest currentRequest)
        {
            var fileRequest = this.requests.Where(r => r.Identifier == currentRequest.Identifier).Single();
            var localSync = new AutoResetEvent(false);

            var completed = false;
            Exception failed = null;

            var startTime = DateTime.Now;
            var webClient = new WebClient();
           
            webClient.DownloadProgressChanged += (sender, args) =>
            {
                if (this.State != DownloaderState.Stopping)
                {
                    this.DownloadUpdated(this, new FileRequestEventArgs(fileRequest));
                    return;
                }
                
                if (currentRequest.FileLength != currentRequest.ContentLength)
                {
                    this.log.Write("ResumableFileDownloader: Interrupted - {0}", currentRequest.UrlAsString);
                }

                webClient.CancelAsync();
                
                localSync.Set();
            };
            
            webClient.OpenReadCompleted += (sender, args) =>
            {
                if (args.Error != null)
                {
                    failed = args.Error;
                }
                else
                {
                    using (var stream = args.Result)
                    {
                        var data = new byte[stream.Length];
                        stream.Read(data, 0, (int) stream.Length);

                        currentRequest.FileLength += stream.Length;

                        var finishTime = DateTime.Now;
                        currentRequest.Duration += finishTime - startTime;

                        FileRequestHelper.WriteToFile(currentRequest, data);
                        
                        this.DownloadUpdated(this, new FileRequestEventArgs(fileRequest));
                    }

                    completed = true;
                }

                localSync.Set();
            };

            webClient.OpenReadAsync(currentRequest.Url);

            localSync.WaitOne();

            if (failed != null)
            {
                throw failed;
            }

            if (completed)
            {
                this.HandleProcessingCompleted(currentRequest);
            }

            if (this.State == DownloaderState.Stopping)
            {
                this.HandleProcessingStopping(currentRequest);
            }
        }

        private void HandleProcessingException(PersistedFileRequest currentRequest, Exception exception)
        {
            currentRequest.State = FileRequestState.Failed;
            currentRequest.Exception = exception;

            this.log.Write("ResumableFileDownloader: Exception, url - '{0}'", currentRequest.UrlAsString);
            this.log.Write("ResumableFileDownloader: Exception, message - '{0}'", exception.Message);

            var fileRequest = this.requests.Where(r => r.Identifier == currentRequest.Identifier).Single();
            this.DownloadFailed(this, new FileRequestEventArgs(fileRequest));
        }

        private void HandleProcessingStopping(PersistedFileRequest currentRequest)
        {
            if (currentRequest.FileLength != currentRequest.ContentLength)
            {
                this.log.Write("ResumableFileDownloader: Interrupted - {0}", currentRequest.UrlAsString);
            }

            var fileRequest = this.requests.Where(r => r.Identifier == currentRequest.Identifier).Single();
            this.DownloadStopped(this, new FileRequestEventArgs(fileRequest));
        }
        
        private void HandleProcessingCompleted(PersistedFileRequest currentRequest)
        {
            currentRequest.State = FileRequestState.Completed;

            this.log.Write("ResumableFileDownloader: Completed - {0}", currentRequest.UrlAsString);

            var fileRequest = this.requests.Where(r => r.Identifier == currentRequest.Identifier).Single();
            this.DownloadCompleted(this, new FileRequestEventArgs(fileRequest));
        }
    }
}